/**
 * Copyright (c) 2018-2028, LookAhead.
 * <p>
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/lgpl.html
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.lookahead.parent.common.utils;

import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.lookahead.parent.apidoc.constant.RoleConstant;
import org.lookahead.parent.common.constant.SecureConstant;
import org.lookahead.parent.common.constant.TokenConstant;
import org.lookahead.parent.common.exceptions.SecureException;
import org.lookahead.parent.common.secure.TokenInfo;
import org.lookahead.parent.common.secure.Userinfo;
import org.lookahead.parent.common.constant.AuthClientEnum;

import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.*;

/**
 * Secure工具类
 *
 * @author LookAhead
 */
@Slf4j
public class SecureUtil {

    private static final String la_SYSTEM_REQUEST_ATTR = "_la_SYSTEM_REQUEST_ATTR_";

    private final static String TOKEN = TokenConstant.TOKEN;
    private final static String BEARER = TokenConstant.BEARER;
    private final static String ACCOUNT = TokenConstant.ACCOUNT;
    private final static String USER_ID = TokenConstant.USER_ID;
    private final static String ROLE_ID = TokenConstant.ROLE_ID;
    private final static String USER_NAME = TokenConstant.USER_NAME;
    private final static String ROLE_NAME = TokenConstant.ROLE_NAME;
    private final static String CLIENT_ID = TokenConstant.CLIENT_ID;
    private final static Integer AUTH_LENGTH = TokenConstant.AUTH_LENGTH;
    private static final String BASE64_SECURITY = Base64.getEncoder().encodeToString(TokenConstant.SIGN_KEY.getBytes(StandardCharsets.UTF_8));
    private final static String REAL_NAME = TokenConstant.REAL_NAME;

    /**
     * 获取用户信息
     *
     * @return Userinfo
     */
    public static Userinfo getUser() {
        HttpServletRequest request = WebUtil.getRequest();
        if (request == null) {
            return null;
        }
        // 优先从 request 中获取
        Object userinfo = request.getAttribute(la_SYSTEM_REQUEST_ATTR);
        if (userinfo == null) {
            userinfo = getUser(request);
            if (userinfo != null) {
                // 设置到 request 中
                request.setAttribute(la_SYSTEM_REQUEST_ATTR, userinfo);
            }
        }
        return (Userinfo) userinfo;
    }

    /**
     * 获取用户信息
     *
     * @param request request
     * @return Userinfo
     */
    public static Userinfo getUser(HttpServletRequest request) {
        Claims claims = getClaims(request);
        if (claims == null) {
            return null;
        }
        return getUserFromClaims(claims);
    }

    /**
     * 获取用户信息
     *
     * @param claims claims
     * @return userinfo
     */
    public static Userinfo getUserFromClaims(Claims claims) {
        return new Userinfo()
                .setClientId(Func.toStr(claims.get(SecureUtil.CLIENT_ID)))
                .setUserId(Func.toLong(claims.get(SecureUtil.USER_ID)))
                .setUserName(Func.toStr(claims.get(SecureUtil.USER_NAME)))
                .setAccount(Func.toStr(claims.get(SecureUtil.ACCOUNT)))
                .setRealName(Func.toStr(claims.get(SecureUtil.REAL_NAME)))
                .setRoleId(Func.toStr(claims.get(SecureUtil.ROLE_ID)))
                .setRoleName(Func.toStr(claims.get(SecureUtil.ROLE_NAME)));
    }

    /**
     * 是否为超管
     *
     * @return boolean
     */
    public static boolean isAdministrator() {
        return StringUtil.containsAny(getUserRole(), RoleConstant.ADMIN);
    }

    /**
     * 获取用户id
     *
     * @return userId
     */
    public static Long getUserId() {
        Userinfo user = getUser();
        return Optional.ofNullable(user).map(Userinfo::getUserId).orElse(-1L);
    }

    /**
     * 获取用户id
     *
     * @param request request
     * @return userId
     */
    public static Long getUserId(HttpServletRequest request) {
        Userinfo user = getUser(request);
        return Optional.ofNullable(user).map(Userinfo::getUserId).orElse(-1L);
    }

    /**
     * 获取用户账号
     *
     * @return userAccount
     */
    public static String getUserAccount() {
        Userinfo user = getUser();
        return Optional.ofNullable(user).map(Userinfo::getAccount).orElse(StringPool.EMPTY);
    }

    /**
     * 获取用户账号
     *
     * @param request request
     * @return userAccount
     */
    public static String getUserAccount(HttpServletRequest request) {
        Userinfo user = getUser(request);
        return Optional.ofNullable(user).map(Userinfo::getAccount).orElse(StringPool.EMPTY);
    }

    /**
     * 获取用户名
     *
     * @return userName
     */
    public static String getUserName() {
        Userinfo user = getUser();
        return Optional.ofNullable(user).map(Userinfo::getUserName).orElse(StringPool.EMPTY);
    }

    /**
     * 获取用户名
     *
     * @param request request
     * @return userName
     */
    public static String getUserName(HttpServletRequest request) {
        Userinfo user = getUser(request);
        return Optional.ofNullable(user).map(Userinfo::getUserName).orElse(StringPool.EMPTY);
    }

    /**
     * 获取真实姓名
     *
     * @return realName
     */
    public static String getRealName() {
        Userinfo user = getUser();
        return Optional.ofNullable(user).map(Userinfo::getRealName).orElse(StringPool.EMPTY);
    }

    /**
     * 获取真实姓名
     *
     * @return realName
     */
    public static String getRealName(HttpServletRequest request) {
        Userinfo user = getUser(request);
        return Optional.ofNullable(user).map(Userinfo::getRealName).orElse(StringPool.EMPTY);
    }

    /**
     * 获取用户角色
     *
     * @return userName
     */
    public static String getUserRole() {
        Userinfo user = getUser();
        return Optional.ofNullable(user).map(Userinfo::getRoleName).orElse(StringPool.EMPTY);
    }

    /**
     * 获取用角色
     *
     * @param request request
     * @return userName
     */
    public static String getUserRole(HttpServletRequest request) {
        Userinfo user = getUser(request);
        return Optional.ofNullable(user).map(Userinfo::getRoleName).orElse(StringPool.EMPTY);
    }


    /**
     * 获取客户端id
     *
     * @return tenantId
     */
    public static String getClientId() {
        Userinfo user = getUser();
        return Optional.ofNullable(user).map(Userinfo::getClientId).orElse(StringPool.EMPTY);
    }

    /**
     * 获取客户端id
     *
     * @param request request
     * @return tenantId
     */
    public static String getClientId(HttpServletRequest request) {
        Userinfo user = getUser(request);
        return Optional.ofNullable(user).map(Userinfo::getClientId).orElse(StringPool.EMPTY);
    }

    /**
     * 获取Claims
     *
     * @param request request
     * @return Claims
     */
    public static Claims getClaims(HttpServletRequest request) {
        String auth = request.getHeader(SecureUtil.TOKEN);
        if (StringUtil.isNotBlank(auth) && auth.length() > AUTH_LENGTH) {
            String headStr = auth.substring(0, 6).toLowerCase();
            if (Objects.equals(headStr, SecureUtil.BEARER)) {
                auth = auth.substring(7);
                return SecureUtil.parseJWT(auth);
            }
        } else {
            String parameter = request.getParameter(SecureUtil.TOKEN);
            if (StringUtil.isNotBlank(parameter)) {
                return SecureUtil.parseJWT(parameter);
            }
        }
        return null;
    }

    /**
     * 获取请求的token
     *
     * @return token
     */
    public static String getToken() {
        String auth = getHeaderToken();
        if ((auth != null) && (auth.length() > AUTH_LENGTH)) {
            String headStr = auth.substring(0, 6).toLowerCase();
            if (Objects.equals(headStr, SecureUtil.BEARER)) {
                auth = auth.substring(7);
            }
            return auth;
        }
        return null;
    }

    /**
     * 获取请求头
     *
     * @return header
     */
    public static String getHeaderToken() {
        return getHeaderToken(Objects.requireNonNull(WebUtil.getRequest()));
    }

    /**
     * 获取请求头
     *
     * @param request request
     * @return header
     */
    public static String getHeaderToken(HttpServletRequest request) {
        return request.getHeader(TOKEN);
    }

    /**
     * 解析jsonWebToken
     *
     * @param jsonWebToken jsonWebToken
     * @return Claims
     */
    public static Claims parseJWT(String jsonWebToken) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(Base64.getDecoder().decode(BASE64_SECURITY))
                    .build()
                    .parseClaimsJws(jsonWebToken)
                    .getBody();
        } catch (Exception ex) {
            log.error(ex.getMessage());
            return null;
        }
    }

    /**
     * 交换accessToken
     *
     * @param second token的过期秒数，若token有效期小于秒数，则重新生成token
     * @return TokenInfo
     */
    public static TokenInfo changeJWT(Integer second) {
        String token = getToken();
        return changeJWT(token, second);
    }

    /**
     * 交换accessToken
     *
     * @param accessToken 现在的accessToken
     * @param second      token的过期秒数，若token有效期小于秒数，则重新生成token
     * @return TokenInfo
     */
    public static TokenInfo changeJWT(String accessToken, Integer second) {
        Claims claims = parseJWT(accessToken);
        if (Objects.isNull(claims)) {
            return null;
        }
        Date expiration = claims.getExpiration();
        int exp = Long.valueOf(DateUtil.between(DateUtil.date(), expiration, DateUnit.SECOND, false)).intValue();
        if (exp < second) {
            Userinfo userinfo = getUserFromClaims(claims);
            // 设置jwt参数
            Map<String, String> param = new HashMap<>();
            param.put(TokenConstant.TOKEN_TYPE, TokenConstant.ACCESS_TOKEN);
            param.put(TokenConstant.USER_ID, Func.toStr(userinfo.getUserId()));
            param.put(TokenConstant.ROLE_ID, Func.toStr(userinfo.getRoleId()));
            param.put(TokenConstant.ACCOUNT, Func.toStr(userinfo.getAccount()));
            param.put(TokenConstant.USER_NAME, Func.toStr(userinfo.getAccount()));
            param.put(TokenConstant.REAL_NAME, Func.toStr(userinfo.getRealName()));
            param.put(TokenConstant.ROLE_NAME, Func.toStr(userinfo.getRoleName()));
            log.info("createAuthInfo param :{}", JSON.toJSONString(param));
            AuthClientEnum authClientEnum = AuthClientEnum.getByClientId(userinfo.getClientId());
            if (Objects.isNull(authClientEnum)) {
                return null;
            }
            // 创建新的access_token
            TokenInfo newAccessToken = createJWT(param, "audience", "issuer", TokenConstant.ACCESS_TOKEN, authClientEnum);
            log.info("create new access token: {}", newAccessToken);
            return newAccessToken;
        }
        // 返回老的access_token信息
        TokenInfo oldAccessToken = new TokenInfo();
        oldAccessToken.setToken(accessToken);
        oldAccessToken.setExpire(exp);
        return oldAccessToken;
    }

    /**
     * 创建令牌
     *
     * @param user      user
     * @param audience  audience  受众
     * @param issuer    issuer   签发人
     * @param tokenType tokenType
     * @return jwt
     */
    public static TokenInfo createJWT(Map<String, String> user, String audience, String issuer, String tokenType, AuthClientEnum authClient) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //生成签名密钥
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        byte[] apiKeySecretBytes = Base64.getDecoder().decode(BASE64_SECURITY);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        //添加构成JWT的类
        JwtBuilder builder = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setIssuer(issuer)
                .setAudience(audience)
                .signWith(signingKey);
        //设置JWT参数
        user.forEach(builder::claim);
        //设置应用id
        builder.claim(CLIENT_ID, authClient.getClientId());
        //添加Token过期时间
        BigDecimal milliseconds = new BigDecimal(1000);
        BigDecimal tokenValidity;
        long expireMillis;
        if (tokenType.equals(TokenConstant.ACCESS_TOKEN)) {
            tokenValidity = BigDecimal.valueOf(authClient.getAccessTokenValidity()).multiply(milliseconds);
            expireMillis = tokenValidity.longValue();
        } else if (tokenType.equals(TokenConstant.REFRESH_TOKEN)) {
            tokenValidity = BigDecimal.valueOf(authClient.getRefreshTokenValidity()).multiply(milliseconds);
            expireMillis = Long.parseLong(tokenValidity.toString());
        } else {
            expireMillis = getExpire();
            tokenValidity = BigDecimal.valueOf(expireMillis);
        }
        long expMillis = nowMillis + expireMillis;
        Date exp = new Date(expMillis);
        builder.setExpiration(exp).setNotBefore(now);
        // 组装Token信息
        TokenInfo tokenInfo = new TokenInfo();
        tokenInfo.setToken(builder.compact());
        tokenInfo.setExpire(tokenValidity.divide(milliseconds, 0, RoundingMode.DOWN).intValue());
        log.debug(tokenInfo.getToken());
        return tokenInfo;
    }

    /**
     * 获取过期时间(次日凌晨3点)
     *
     * @return expire
     */
    public static long getExpire() {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DAY_OF_YEAR, 1);
        cal.set(Calendar.HOUR_OF_DAY, 3);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTimeInMillis() - System.currentTimeMillis();
    }

    /**
     * 客户端信息解码
     */
    @SneakyThrows
    public static String[] extractAndDecodeHeader() {
        // 获取请求头客户端信息
        String header = Objects.requireNonNull(WebUtil.getRequest()).getHeader(SecureConstant.BASIC_HEADER_KEY);
        header = Func.toStr(header).replace(SecureConstant.BASIC_HEADER_PREFIX_EXT, SecureConstant.BASIC_HEADER_PREFIX);
        if (!header.startsWith(SecureConstant.BASIC_HEADER_PREFIX)) {
            throw new SecureException("No client information in request header");
        }
        byte[] base64Token = header.substring(5).getBytes(StandardCharsets.UTF_8);

        byte[] decoded;
        try {
            decoded = Base64.getDecoder().decode(base64Token);
        } catch (IllegalArgumentException var7) {
            throw new RuntimeException("Failed to decode basic authentication token");
        }

        String token = new String(decoded, StandardCharsets.UTF_8);
        int index = token.indexOf(StringPool.COLON);
        if (index == -1) {
            throw new RuntimeException("Invalid basic authentication token");
        } else {
            return new String[]{token.substring(0, index), token.substring(index + 1)};
        }
    }

    public static void main(String[] args) {
        Userinfo user = new Userinfo()
                .setClientId(AuthClientEnum.LOOKAHEAD.getClientId())
                .setUserId(100L)
                .setUserName("admin")
                .setAccount("13800000001")
                .setRealName("管理员")
                .setRoleId("123123")
                .setRoleName("超级管理员");
        Map<String, String> param = new HashMap<>(16);
        param.put(TokenConstant.TOKEN_TYPE, TokenConstant.ACCESS_TOKEN);
        param.put(TokenConstant.USER_ID, Func.toStr(user.getUserId()));
        param.put(TokenConstant.ROLE_ID, Func.toStr(user.getRoleId()));
        param.put(TokenConstant.ACCOUNT, Func.toStr(user.getAccount()));
        param.put(TokenConstant.USER_NAME, Func.toStr(user.getUserName()));
        param.put(TokenConstant.REAL_NAME, Func.toStr(user.getRealName()));
        param.put(TokenConstant.ROLE_NAME, Func.toStr(user.getRoleName()));
        TokenInfo accessToken = createJWT(param, "audience", "issuer", TokenConstant.ACCESS_TOKEN, AuthClientEnum.LOOKAHEAD);
        System.out.println(accessToken);

        System.out.println("\n------------------\n");
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1c2VyIiwiYXVkIjoiYXVkaWVuY2UiLCJyb2xlX25hbWUiOiJhZG1pbizllYbliqHnu4_nkIYs6L-Q57u05Lq65ZGYIiwidXNlcl9pZCI6IjE2NzY1MTU2OTUxMzcwNDI0MzMiLCJyb2xlX2lkIjoiMTQzMDQ2MDE5ODE2NTcxNjk5NCwxNDE0NDU2ODAzODE0NTQzMzYyLDE0MzExMzc2MTMxODM0Nzk4MDkiLCJ1c2VyX25hbWUiOiIxODYwMTM3NzI1MiIsInJlYWxfbmFtZSI6IuabsuWBpeWugSIsInRva2VuX3R5cGUiOiJhY2Nlc3NfdG9rZW4iLCJhY2NvdW50IjoiMTg2MDEzNzcyNTIiLCJjbGllbnRfaWQiOiJsb29rYWhlYWQiLCJleHAiOjE3MjQ2MzA0ODksIm5iZiI6MTcyNDU0NDA4OX0.69xBycI5NKjYqtV3jZGobR2Ghq6ov0IOPtk9YyhxH0s";
        Claims claims = parseJWT(token);
        System.out.println(JSON.toJSONString(claims));
        Userinfo userinfo = null;
        if (claims != null) {
            userinfo = getUserFromClaims(claims);
        }
        if (userinfo != null) {
            System.out.println(userinfo);
            System.out.println(userinfo.getUserId());
            System.out.println(userinfo.getRealName());
        }

        System.out.println("\n------------------\n");
        System.out.println(token);
        TokenInfo tokenInfo = changeJWT(token, 60 * 60 * 24);
        System.out.println(tokenInfo);
        System.out.println(JSON.toJSONString(parseJWT(tokenInfo.getToken())));

    }

}
